package jmine.tec.rpc.server;

import static javax.servlet.http.HttpServletResponse.SC_BAD_REQUEST;

import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.math.BigDecimal;
import java.text.ParseException;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import jmine.tec.rpc.RPCMessages;
import jmine.tec.rpc.common.RPCException;
import jmine.tec.rpc.common.request.RestRequest;
import jmine.tec.rpc.common.request.RestResponse;
import jmine.tec.rpc.common.serialization.Serializer;
import jmine.tec.web.servlet.FilterServletInputStream;
import jmine.tec.web.servlet.FilterServletOutputStream;
import jmine.tec.web.servlet.rest.RestService;

import org.apache.commons.io.output.ByteArrayOutputStream;
import org.jmock.Expectations;
import org.jmock.Mockery;
import org.jmock.integration.junit4.JMock;
import org.jmock.integration.junit4.JUnit4Mockery;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;

/**
 * Teste para o server stub
 * 
 * @author lundberg
 */
@RunWith(JMock.class)
public class AServerStubShould {

    private final Mockery context = new JUnit4Mockery();

    private final Serializer serializer = this.context.mock(Serializer.class);

    private MockRemoteServerService implementor = this.context.mock(MockRemoteServerService.class);

    private HttpServletRequest request = this.context.mock(HttpServletRequest.class);

    private HttpServletResponse response = this.context.mock(HttpServletResponse.class);

    private final Class<MockRemoteServerService> service = MockRemoteServerService.class;

    private final RestRequest serviceRequest = new RestRequest("reverse", new Class<?>[]{ String.class }, new Object[]{ "abc" });

    private final InputStream requestBody = new FilterServletInputStream(new ByteArrayInputStream(new byte[]{ 1, 2, 3 }));

    private final InputStream serializedResult = new ByteArrayInputStream(new byte[]{ 3, 2, 1 });

    private final OutputStream responseBody = new FilterServletOutputStream(new ByteArrayOutputStream());

    private RestService stub;

    /**
     * Init
     */
    @Before
    public void init() {
        this.stub =
                ServerStubBuilder.<MockRemoteServerService> aServerStub().with(this.serializer).with(this.service).with(this.implementor)
                        .withPrefix("mock").build();
    }

    /**
     * Tests request dispatching
     * 
     * @throws IOException IOException
     * @throws ParseException ParseException
     */
    @Test
    public void receiveRequest() throws IOException, ParseException {
        this.context.checking(new Expectations() {
            {
                this.oneOf(AServerStubShould.this.request).getInputStream();
                this.will(returnValue(AServerStubShould.this.requestBody));

                this.oneOf(AServerStubShould.this.serializer).deserialize(AServerStubShould.this.requestBody);
                this.will(returnValue(AServerStubShould.this.serviceRequest));

                this.oneOf(AServerStubShould.this.implementor).reverse("abc");
                this.will(returnValue("cba"));

                this.oneOf(AServerStubShould.this.serializer).serialize(new RestResponse("cba"));
                this.will(returnValue(AServerStubShould.this.serializedResult));

                this.oneOf(AServerStubShould.this.response).getOutputStream();
                this.will(returnValue(AServerStubShould.this.responseBody));
            }
        });
        this.stub.post(this.request, this.response);
    }

    /**
     * Invalid data handling
     * 
     * @throws IOException IOException
     * @throws ParseException ParseException
     */
    @Test
    public void sendErrorOnInvalidInput() throws IOException, ParseException {
        this.context.checking(new Expectations() {
            {
                this.oneOf(AServerStubShould.this.request).getInputStream();
                this.will(returnValue(AServerStubShould.this.requestBody));

                this.oneOf(AServerStubShould.this.serializer).deserialize(AServerStubShould.this.requestBody);
                this.will(returnValue(BigDecimal.ONE));

                this.oneOf(AServerStubShould.this.response).sendError(HttpServletResponse.SC_BAD_REQUEST,
                        "<jterror>Invalid request input.</jterror>");
            }
        });
        this.stub.post(this.request, this.response);
    }

    /**
     * Invalid data handling
     * 
     * @throws IOException IOException
     * @throws ParseException ParseException
     */
    @Test
    public void sendErrorOnInvalidFormat() throws IOException, ParseException {
        this.context.checking(new Expectations() {
            {
                this.oneOf(AServerStubShould.this.request).getInputStream();
                this.will(returnValue(AServerStubShould.this.requestBody));

                this.oneOf(AServerStubShould.this.serializer).deserialize(AServerStubShould.this.requestBody);
                this.will(throwException(new ParseException("invalid", 0)));

                this.oneOf(AServerStubShould.this.response).sendError(HttpServletResponse.SC_BAD_REQUEST,
                        "<jterror>Invalid request format.</jterror>");
            }
        });
        this.stub.post(this.request, this.response);
    }

    /**
     * Throws IOException
     * 
     * @throws IOException IOException
     * @throws ParseException ParseException
     */
    @Test(expected = IOException.class)
    public void notCatchIOException() throws IOException, ParseException {
        this.context.checking(new Expectations() {
            {
                this.oneOf(AServerStubShould.this.request).getInputStream();
                this.will(returnValue(AServerStubShould.this.requestBody));

                this.oneOf(AServerStubShould.this.serializer).deserialize(AServerStubShould.this.requestBody);
                this.will(throwException(new IOException()));
            }
        });
        this.stub.post(this.request, this.response);
    }

    /**
     * Exception handling test
     * 
     * @throws IOException IOException
     * @throws ParseException ParseException
     */
    @Test
    public void catchNoSuchMethodExceptionWrongName() throws IOException, ParseException {
        this.serviceRequest.setMethod("notAMethod");
        this.context.checking(new Expectations() {
            {
                this.oneOf(AServerStubShould.this.request).getInputStream();
                this.will(returnValue(AServerStubShould.this.requestBody));

                this.oneOf(AServerStubShould.this.serializer).deserialize(AServerStubShould.this.requestBody);
                this.will(returnValue(AServerStubShould.this.serviceRequest));

                this.oneOf(AServerStubShould.this.response).sendError(SC_BAD_REQUEST, "<jterror>Requested method not found.</jterror>");
            }
        });
        this.stub.post(this.request, this.response);
    }

    /**
     * Exception handling test
     * 
     * @throws IOException IOException
     * @throws ParseException ParseException
     */
    @Test
    public void catchNoSuchMethodExceptionWrongTypes() throws IOException, ParseException {
        this.serviceRequest.setParameterTypes(new Class<?>[]{});
        this.context.checking(new Expectations() {
            {
                this.oneOf(AServerStubShould.this.request).getInputStream();
                this.will(returnValue(AServerStubShould.this.requestBody));

                this.oneOf(AServerStubShould.this.serializer).deserialize(AServerStubShould.this.requestBody);
                this.will(returnValue(AServerStubShould.this.serviceRequest));

                this.oneOf(AServerStubShould.this.response).sendError(HttpServletResponse.SC_BAD_REQUEST,
                        "<jterror>Requested method not found.</jterror>");
            }
        });
        this.stub.post(this.request, this.response);
    }

    /**
     * Exception handling test
     * 
     * @throws IOException IOException
     * @throws ParseException ParseException
     */
    @Test
    public void catchIllegalArgumentException() throws IOException, ParseException {
        this.serviceRequest.setParameters(new Object[]{ BigDecimal.ONE });
        this.context.checking(new Expectations() {
            {
                this.oneOf(AServerStubShould.this.request).getInputStream();
                this.will(returnValue(AServerStubShould.this.requestBody));

                this.oneOf(AServerStubShould.this.serializer).deserialize(AServerStubShould.this.requestBody);
                this.will(returnValue(AServerStubShould.this.serviceRequest));

                this.oneOf(AServerStubShould.this.response).sendError(HttpServletResponse.SC_BAD_REQUEST,
                        "<jterror>Illegal arguments provided.</jterror>");
            }
        });
        this.stub.post(this.request, this.response);
    }

    /**
     * Exception handling test
     * 
     * @throws IOException IOException
     * @throws ParseException ParseException
     */
    @Test
    public void catchExceptionInsideService() throws IOException, ParseException {
        final RuntimeException e = new RuntimeException();
        this.context.checking(new Expectations() {
            {
                this.oneOf(AServerStubShould.this.request).getInputStream();
                this.will(returnValue(AServerStubShould.this.requestBody));

                this.oneOf(AServerStubShould.this.serializer).deserialize(AServerStubShould.this.requestBody);
                this.will(returnValue(AServerStubShould.this.serviceRequest));

                this.oneOf(AServerStubShould.this.implementor).reverse("abc");
                this.will(throwException(e));

                this.oneOf(AServerStubShould.this.serializer).serialize(new RestResponse(e));
                this.will(returnValue(AServerStubShould.this.serializedResult));

                this.oneOf(AServerStubShould.this.response).getOutputStream();
                this.will(returnValue(AServerStubShould.this.responseBody));
            }
        });
        this.stub.post(this.request, this.response);
    }

    /**
     * Exception handling test
     * 
     * @throws IOException IOException
     * @throws ParseException ParseException
     */
    @Test
    public void catchUnexpectedExceptionInvokingMethod() throws IOException, ParseException {
        this.context.checking(new Expectations() {
            {
                this.oneOf(AServerStubShould.this.request).getInputStream();
                this.will(returnValue(AServerStubShould.this.requestBody));

                this.oneOf(AServerStubShould.this.serializer).deserialize(AServerStubShould.this.requestBody);
                this.will(throwException(new RPCException(RPCMessages.JSON_ERROR_CREATING_BEAN.create())));

                this.oneOf(AServerStubShould.this.response).sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR,
                        "<jterror>Internal server error.</jterror>");
            }
        });
        this.stub.post(this.request, this.response);
    }

    /**
     * Testa rejeição de classes
     */
    @Test(expected = RPCException.class)
    public void rejectClassService() {
        this.stub = ServerStubBuilder.<ClassServerService> aServerStub().with(ClassServerService.class).build();
    }

    /**
     * Not Allowed method request
     * 
     * @throws IOException IOException
     */
    @Test
    public void rejectPut() throws IOException {
        this.context.checking(new Expectations() {
            {
                this.oneOf(AServerStubShould.this.response).sendError(HttpServletResponse.SC_METHOD_NOT_ALLOWED, "<jterror>mock</jterror>");
            }
        });
        this.stub.put(this.request, this.response);
    }

    /**
     * Not Allowed method request
     * 
     * @throws IOException IOException
     */
    @Test
    public void rejectDelete() throws IOException {
        this.context.checking(new Expectations() {
            {
                this.oneOf(AServerStubShould.this.response).sendError(HttpServletResponse.SC_METHOD_NOT_ALLOWED, "<jterror>mock</jterror>");
            }
        });
        this.stub.delete(this.request, this.response);
    }

    /**
     * Not Allowed method request
     * 
     * @throws IOException IOException
     */
    @Test
    public void rejectGet() throws IOException {
        this.context.checking(new Expectations() {
            {
                this.oneOf(AServerStubShould.this.response).sendError(HttpServletResponse.SC_METHOD_NOT_ALLOWED, "<jterror>mock</jterror>");
            }
        });
        this.stub.get(this.request, this.response);
    }
}
